using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace LightCAD.Three
{
    public class ExtrudeGeometry : BufferGeometry
    {
        public class ExtrudeParameters : JsObj<object>
        {
            public ListEx<Shape> shapes { get => this["shapes"] as ListEx<Shape>; set { this["shapes"] = value; } }
            public ExtrudeOption options { get => this["options"] as ExtrudeOption; set { this["options"] = value; } }
        }
        public ExtrudeParameters parameters;
        public class ExtrudeOption
        {
            public int? curveSegments = 12;
            public int? steps = 1;
            public double? depth = 1;
            public bool? bevelEnabled = true;
            public double? bevelThickness = 0.2;
            public double? bevelSize;
            public double? bevelOffset = 0;
            public int? bevelSegments = 3;
            public CurvePath<Vector3> extrudePath;
            public IExtrudeUVGenerator UVGenerator;
        }
        public ExtrudeGeometry()
        {

        }
        public ExtrudeGeometry(ListEx<Shape> shapes, ExtrudeOption options = null)
        {
            init(shapes, options);
        }
        public void init(ListEx<Shape> shapes, ExtrudeOption options)
        {
            if (options == null)
                options = new ExtrudeOption();
            this.parameters = new ExtrudeParameters { shapes = shapes, options = options };

            var scope = this;

            var verticesArray = new ListEx<double>();
            var uvArray = new ListEx<double>();

            for (int i = 0, l = shapes.Length; i < l; i++)
            {

                var shape = shapes[i];
                addShape(shape);

            }
            // build geometry

            this.setAttribute("position", new Float32BufferAttribute(verticesArray.ToArray(), 3));
            this.setAttribute("uv", new Float32BufferAttribute(uvArray.ToArray(), 2));

            this.computeVertexNormals();

            // functions

            void addShape(Shape shape)
            {

                ListEx<double> placeholder = new ListEx<double>();

                // options

                var curveSegments = options.curveSegments.Value;
                var steps = options.steps.Value;
                var depth = options.depth.Value;

                var bevelEnabled = options.bevelEnabled.Value;
                var bevelThickness = options.bevelThickness.Value;
                var bevelSize = options.bevelSize != null ? options.bevelSize.Value : bevelThickness - 0.1;
                var bevelOffset = options.bevelOffset.Value;
                var bevelSegments = options.bevelSegments.Value;

                var extrudePath = options.extrudePath;

                var uvgen = options.UVGenerator != null ? options.UVGenerator : WorldUVGeneratorClass.WorldUVGenerator;

                //

                ListEx<Vector3> extrudePts = null;
                var extrudeByPath = false;
                FrenetFrame splineTube = null;
                Vector3 binormal = null, normal = null;
                Vector3 position2 = null;

                if (extrudePath != null)
                {

                    extrudePts = extrudePath.getSpacedPoints(steps);

                    extrudeByPath = true;
                    bevelEnabled = false; // bevels not supported for path extrusion

                    // SETUP TNB variables

                    // TODO1 - have a .isClosed in spline?

                    splineTube = extrudePath.computeFrenetFrames(steps, false);

                    // console.log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);

                    binormal = new Vector3();
                    normal = new Vector3();
                    position2 = new Vector3();

                }

                // Safeguards if bevels are not enabled

                if (!bevelEnabled)
                {

                    bevelSegments = 0;
                    bevelThickness = 0;
                    bevelSize = 0;
                    bevelOffset = 0;

                }

                // Variables initialization

                var shapePoints = shape.extractPoints(curveSegments);

                var vertices = shapePoints.shape;
                var holes = shapePoints.holes;

                var reverse = !ShapeUtils.isClockWise(vertices);

                if (reverse)
                {

                     vertices.Reverse();

                    // Maybe we should also check if holes are in the opposite direction, just to be safe ...

                    for (int h = 0, hl = holes.Length; h < hl; h++)
                    {

                        var ahole = holes[h];

                        if (ShapeUtils.isClockWise(ahole))
                        {
                            ahole.Reverse();
                            holes[h] = ahole;
                        }
                    }
                }


                var faces = ShapeUtils.triangulateShape(vertices, holes);

                /* Vertices */

                var contour = vertices; // vertices has all points but contour has only points of circumference

                for (int h = 0, hl = holes.Length; h < hl; h++)
                {
                    var ahole = holes[h];

                    vertices = vertices.Concat(ahole);

                }


                Vector2 scalePt2(Vector2 pt, Vector2 vec, double size)
                {

                    if (vec == null) console.error("THREE.ExtrudeGeometry: vec does not exist");

                    return pt.Clone().AddScaledVector(vec, size);

                }

                int vlen = vertices.Length, flen = faces.Length;


                // Find directions for point movement


                Vector2 getBevelVec(Vector2 inPt, Vector2 inPrev, Vector2 inNext)
                {

                    // computes for inPt the corresponding point inPt' on a new contour
                    //   shifted by 1 unit (length of normalized vector) to the left
                    // if we walk along contour clockwise, this new contour is outside the old one
                    //
                    // inPt' is the intersection of the two lines parallel to the two
                    //  adjacent edges of inPt at a distance of 1 unit on the left side.

                    double v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt

                    // good reading for geometry algorithms (here: line-line intersection)
                    // http://geomalgorithms.com/a05-_intersect-1.html

                    var v_prev_x = inPt.X - inPrev.X;
                    var v_prev_y = inPt.Y - inPrev.Y;
                    var v_next_x = inNext.X - inPt.X;
                    var v_next_y = inNext.Y - inPt.Y;

                    var v_prev_lensq = (v_prev_x * v_prev_x + v_prev_y * v_prev_y);

                    // check for collinear edges
                    var collinear0 = (v_prev_x * v_next_y - v_prev_y * v_next_x);

                    if (Math.Abs(collinear0) > MathEx.EPSILON)
                    {

                        // not collinear

                        // length of vectors for normalizing

                        var v_prev_len = Math.Sqrt(v_prev_lensq);
                        var v_next_len = Math.Sqrt(v_next_x * v_next_x + v_next_y * v_next_y);

                        // shift adjacent points by unit vectors to the left

                        var ptPrevShift_x = (inPrev.X - v_prev_y / v_prev_len);
                        var ptPrevShift_y = (inPrev.Y + v_prev_x / v_prev_len);

                        var ptNextShift_x = (inNext.X - v_next_y / v_next_len);
                        var ptNextShift_y = (inNext.Y + v_next_x / v_next_len);

                        // scaling factor for v_prev to intersection point

                        var sf = ((ptNextShift_x - ptPrevShift_x) * v_next_y -
                                (ptNextShift_y - ptPrevShift_y) * v_next_x) /
                            (v_prev_x * v_next_y - v_prev_y * v_next_x);

                        // vector from inPt to intersection point

                        v_trans_x = (ptPrevShift_x + v_prev_x * sf - inPt.X);
                        v_trans_y = (ptPrevShift_y + v_prev_y * sf - inPt.Y);

                        // Don't Normalize!, otherwise sharp corners become ugly
                        //  but prevent crazy spikes
                        var v_trans_lensq = (v_trans_x * v_trans_x + v_trans_y * v_trans_y);
                        if (v_trans_lensq <= 2)
                        {

                            return new Vector2(v_trans_x, v_trans_y);

                        }
                        else
                        {

                            shrink_by = Math.Sqrt(v_trans_lensq / 2);

                        }

                    }
                    else
                    {

                        // handle special case of collinear edges

                        var direction_eq = false; // assumes: opposite

                        if (v_prev_x > MathEx.EPSILON)
                        {

                            if (v_next_x > MathEx.EPSILON)
                            {

                                direction_eq = true;

                            }

                        }
                        else
                        {

                            if (v_prev_x < -MathEx.EPSILON)
                            {

                                if (v_next_x < -MathEx.EPSILON)
                                {

                                    direction_eq = true;

                                }

                            }
                            else
                            {

                                if (Math.Sign(v_prev_y) == Math.Sign(v_next_y))
                                {

                                    direction_eq = true;

                                }

                            }

                        }

                        if (direction_eq)
                        {

                            // console.log("Warning: lines are a straight sequence");
                            v_trans_x = -v_prev_y;
                            v_trans_y = v_prev_x;
                            shrink_by = Math.Sqrt(v_prev_lensq);

                        }
                        else
                        {

                            // console.log("Warning: lines are a straight spike");
                            v_trans_x = v_prev_x;
                            v_trans_y = v_prev_y;
                            shrink_by = Math.Sqrt(v_prev_lensq / 2);

                        }

                    }

                    return new Vector2(v_trans_x / shrink_by, v_trans_y / shrink_by);

                }


                var contourMovements = new ListEx<Vector2>();

                for (int i = 0, il = contour.Length, j = il - 1, k = i + 1; i < il; i++, j++, k++)
                {

                    if (j == il) j = 0;
                    if (k == il) k = 0;

                    //  (j)---(i)---(k)
                    // console.log('i,j,k', i, j , k)

                    contourMovements[i] = getBevelVec(contour[i], contour[j], contour[k]);

                }

                var holesMovements = new ListEx<ListEx<Vector2>>();
                ListEx<Vector2> oneHoleMovements, verticesMovements = contourMovements.Concat();

                for (int h = 0, hl = holes.Length; h < hl; h++)
                {

                    var ahole = holes[h];

                    oneHoleMovements = new ListEx<Vector2>();

                    for (int i = 0, il = ahole.Length, j = il - 1, k = i + 1; i < il; i++, j++, k++)
                    {

                        if (j == il) j = 0;
                        if (k == il) k = 0;

                        //  (j)---(i)---(k)
                        oneHoleMovements[i] = getBevelVec(ahole[i], ahole[j], ahole[k]);

                    }

                    holesMovements.Push(oneHoleMovements);
                    verticesMovements = verticesMovements.Concat(oneHoleMovements);

                }


                // Loop bevelSegments, 1 for the front, 1 for the back

                for (var b = 0; b < bevelSegments; b++)
                {

                    //for ( b = bevelSegments; b > 0; b -- ) {

                    var t = b / (double)bevelSegments;
                    var z = bevelThickness * Math.Cos(t * Math.PI / 2);
                    var _bs = bevelSize * Math.Sin(t * Math.PI / 2) + bevelOffset;

                    // contract shape

                    for (int i = 0, il = contour.Length; i < il; i++)
                    {

                        var vert = scalePt2(contour[i], contourMovements[i], _bs);
                        v(vert.X, vert.Y, -z);

                    }

                    // expand holes

                    for (int h = 0, hl = holes.Length; h < hl; h++)
                    {

                        var ahole = holes[h];
                        oneHoleMovements = holesMovements[h];

                        for (int i = 0, il = ahole.Length; i < il; i++)
                        {

                            var vert = scalePt2(ahole[i], oneHoleMovements[i], _bs);

                            v(vert.X, vert.Y, -z);

                        }

                    }

                }

                var bs = bevelSize + bevelOffset;

                // Back facing vertices

                for (var i = 0; i < vlen; i++)
                {

                    var vert = bevelEnabled ? scalePt2(vertices[i], verticesMovements[i], bs) : vertices[i];

                    if (!extrudeByPath)
                    {

                        v(vert.X, vert.Y, 0);

                    }
                    else
                    {

                        // v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );

                        normal.Copy(splineTube.normals[0]).MulScalar(vert.X);
                        binormal.Copy(splineTube.binormals[0]).MulScalar(vert.Y);

                        position2.Copy(extrudePts[0]).Add(normal).Add(binormal);

                        v(position2.X, position2.Y, position2.Z);

                    }

                }

                // Add stepped vertices...
                // Including front facing vertices

                for (int s = 1; s <= steps; s++)
                {

                    for (int i = 0; i < vlen; i++)
                    {

                        var vert = bevelEnabled ? scalePt2(vertices[i], verticesMovements[i], bs) : vertices[i];

                        if (!extrudeByPath)
                        {

                            v(vert.X, vert.Y, depth / steps * s);

                        }
                        else
                        {

                            // v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );

                            normal.Copy(splineTube.normals[s]).MulScalar(vert.X);
                            binormal.Copy(splineTube.binormals[s]).MulScalar(vert.Y);

                            position2.Copy(extrudePts[s]).Add(normal).Add(binormal);

                            v(position2.X, position2.Y, position2.Z);

                        }

                    }

                }


                // Add bevel segments planes

                //for ( b = 1; b <= bevelSegments; b ++ ) {
                for (int b = bevelSegments - 1; b >= 0; b--)
                {

                    var t = b / (double)bevelSegments;
                    var z = bevelThickness * Math.Cos(t * Math.PI / 2);
                    var _bs = bevelSize * Math.Sin(t * Math.PI / 2) + bevelOffset;

                    // contract shape

                    for (int i = 0, il = contour.Length; i < il; i++)
                    {

                        var vert = scalePt2(contour[i], contourMovements[i], _bs);
                        v(vert.X, vert.Y, depth + z);

                    }

                    // expand holes

                    for (int h = 0, hl = holes.Length; h < hl; h++)
                    {

                        var ahole = holes[h];
                        oneHoleMovements = holesMovements[h];

                        for (int i = 0, il = ahole.Length; i < il; i++)
                        {

                            var vert = scalePt2(ahole[i], oneHoleMovements[i], bs);

                            if (!extrudeByPath)
                            {

                                v(vert.X, vert.Y, depth + z);
                            }
                            else
                            {

                                v(vert.X, vert.Y + extrudePts[steps - 1].Y, extrudePts[steps - 1].X + z);

                            }

                        }

                    }

                }

                /* Faces */

                // Top and bottom faces

                buildLidFaces();

                // Sides faces

                buildSideFaces();


                /////  Internal functions

                void buildLidFaces()
                {

                    var start = verticesArray.Length / 3;

                    if (bevelEnabled)
                    {

                        var layer = 0; // steps + 1
                        var offset = vlen * layer;

                        // Bottom faces

                        for (var i = 0; i < flen; i++)
                        {

                            var face = faces[i];
                            f3(face[2] + offset, face[1] + offset, face[0] + offset);

                        }

                        layer = steps + bevelSegments * 2;
                        offset = vlen * layer;

                        // Top faces

                        for (var i = 0; i < flen; i++)
                        {

                            var face = faces[i];
                            f3(face[0] + offset, face[1] + offset, face[2] + offset);

                        }

                    }
                    else
                    {

                        // Bottom faces

                        for (var i = 0; i < flen; i++)
                        {

                            var face = faces[i];
                            f3(face[2], face[1], face[0]);

                        }

                        // Top faces

                        for (var i = 0; i < flen; i++)
                        {
                            var face = faces[i];
                            f3(face[0] + vlen * steps, face[1] + vlen * steps, face[2] + vlen * steps);
                        }
                    }

                    scope.addGroup(start, verticesArray.Length / 3 - start, 0);

                }

                // Create faces for the z-sides of the shape

                void buildSideFaces()
                {

                    var start = verticesArray.Length / 3;
                    var layeroffset = 0;
                    sidewalls(contour, layeroffset);
                    layeroffset += contour.Length;

                    for (int h = 0, hl = holes.Length; h < hl; h++)
                    {

                        var ahole = holes[h];
                        sidewalls(ahole, layeroffset);

                        //, true
                        layeroffset += ahole.Length;

                    }


                    scope.addGroup(start, verticesArray.Length / 3 - start, 1);


                }

                void sidewalls(ListEx<Vector2> _contour, int layeroffset)
                {

                    var i = _contour.Length;

                    while (--i >= 0)
                    {

                        var j = i;
                        var k = i - 1;
                        if (k < 0) k = _contour.Length - 1;

                        //console.log('b', i,j, i-1, k,vertices.length);

                        for (int s = 0, sl = (steps + bevelSegments * 2); s < sl; s++)
                        {

                            var slen1 = vlen * s;
                            var slen2 = vlen * (s + 1);
                            int a = layeroffset + j + slen1,
                            b = layeroffset + k + slen1,
                                c = layeroffset + k + slen2,
                                d = layeroffset + j + slen2;
                            f4(a, b, c, d);
                        }

                    }

                }

                void v(double x, double y, double z)
                {

                    placeholder.Push(x);
                    placeholder.Push(y);
                    placeholder.Push(z);

                }


                void f3(int a, int b, int c)
                {

                    addVertex(a);
                    addVertex(b);
                    addVertex(c);

                    var nextIndex = verticesArray.Length / 3;
                    var uvs = uvgen.generateTopUV(scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1);

                    addUV(uvs[0]);
                    addUV(uvs[1]);
                    addUV(uvs[2]);

                }

                void f4(int a, int b, int c, int d)
                {

                    addVertex(a);
                    addVertex(b);
                    addVertex(d);

                    addVertex(b);
                    addVertex(c);
                    addVertex(d);


                    var nextIndex = verticesArray.Length / 3;
                    var uvs = uvgen.generateSideWallUV(scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1);

                    addUV(uvs[0]);
                    addUV(uvs[1]);
                    addUV(uvs[3]);

                    addUV(uvs[1]);
                    addUV(uvs[2]);
                    addUV(uvs[3]);

                }

                void addVertex(int index)
                {

                    verticesArray.Push(placeholder[index * 3 + 0]);
                    verticesArray.Push(placeholder[index * 3 + 1]);
                    verticesArray.Push(placeholder[index * 3 + 2]);

                }


                void addUV(Vector2 vector2)
                {

                    uvArray.Push(vector2.X);
                    uvArray.Push(vector2.Y);

                }

            }
        }
        private static ListEx<Shape> initShapes(Shape shape = null)
        {
            if (shape == null)
                shape = new Shape(new ListEx<Vector2> { new Vector2(0.5, 0.5), new Vector2(-0.5, 0.5), new Vector2(-0.5, -0.5), new Vector2(0.5, -0.5) });
            return new ListEx<Shape> { shape };
        }
        public ExtrudeGeometry(Shape shape = null, ExtrudeOption options = null) : this(initShapes(shape), options)
        {
        }

        public ExtrudeGeometry copy(ExtrudeGeometry source)
        {
            base.copy(source);
            this.parameters = new ExtrudeParameters()
            {
                shapes = source.parameters.shapes,
                options = source.parameters.options,
            };
            return this;
        }

    }
    public interface IExtrudeUVGenerator
    {
        ListEx<Vector2> generateTopUV(BufferGeometry geometry, ListEx<double> vertices, int indexA, int indexB, int indexC);
        ListEx<Vector2> generateSideWallUV(BufferGeometry geometry, ListEx<double> vertices, int indexA, int indexB, int indexC, int indexD);

    }
    public sealed class WorldUVGeneratorClass : IExtrudeUVGenerator
    {
        public static WorldUVGeneratorClass WorldUVGenerator = new WorldUVGeneratorClass();

        public ListEx<Vector2> generateTopUV(BufferGeometry geometry, ListEx<double> vertices, int indexA, int indexB, int indexC)
        {


            var a_x = vertices[indexA * 3];
            var a_y = vertices[indexA * 3 + 1];
            var b_x = vertices[indexB * 3];
            var b_y = vertices[indexB * 3 + 1];
            var c_x = vertices[indexC * 3];
            var c_y = vertices[indexC * 3 + 1];

            return new ListEx<Vector2> {

            new Vector2(a_x, a_y ),
            new Vector2(b_x, b_y ),
            new Vector2(c_x, c_y )
        };

        }

        public ListEx<Vector2> generateSideWallUV(BufferGeometry geometry, ListEx<double> vertices, int indexA, int indexB, int indexC, int indexD)
        {

            var a_x = vertices[indexA * 3];
            var a_y = vertices[indexA * 3 + 1];
            var a_z = vertices[indexA * 3 + 2];
            var b_x = vertices[indexB * 3];
            var b_y = vertices[indexB * 3 + 1];
            var b_z = vertices[indexB * 3 + 2];
            var c_x = vertices[indexC * 3];
            var c_y = vertices[indexC * 3 + 1];
            var c_z = vertices[indexC * 3 + 2];
            var d_x = vertices[indexD * 3];
            var d_y = vertices[indexD * 3 + 1];
            var d_z = vertices[indexD * 3 + 2];

            if (Math.Abs(a_y - b_y) < Math.Abs(a_x - b_x))
            {

                return new ListEx<Vector2>{
                    new Vector2(a_x, 1 - a_z),
                    new Vector2(b_x, 1 - b_z),
                    new Vector2(c_x, 1 - c_z),
                    new Vector2(d_x, 1 - d_z)
                };

            }
            else
            {

                return new ListEx<Vector2>{
                    new Vector2(a_y, 1 - a_z),
                    new Vector2(b_y, 1 - b_z),
                new Vector2(c_y, 1 - c_z),
                    new Vector2(d_y, 1 - d_z)
                };
            }

        }

    };
}
